/**
 * OSAmI: Open Source Ambient Intelligence
 * Copyright (C) 2010 LCIS - CTSYS - INPG
 * Contact: osami-general@osami-commons.org
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 * USA
 */

package org.osami.gateway.sensor.internal;

import java.util.ArrayList;
import java.util.Dictionary;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.logging.Logger;

import org.apache.felix.ipojo.ComponentInstance;
import org.apache.felix.ipojo.Factory;
import org.apache.felix.ipojo.annotations.Bind;
import org.apache.felix.ipojo.annotations.Component;
import org.apache.felix.ipojo.annotations.Invalidate;
import org.apache.felix.ipojo.annotations.Provides;
import org.apache.felix.ipojo.annotations.Requires;
import org.apache.felix.ipojo.annotations.Unbind;
import org.apache.felix.ipojo.annotations.Validate;
import org.osami.gateway.sensor.internal.message.ByePayload;
import org.osami.gateway.sensor.internal.message.HelloPayload;
import org.osami.gateway.sensor.internal.message.MetadataPayload;
import org.osami.gateway.sensor.internal.multicast.MulticastOutboundMessage;
import org.osami.gateway.sensor.protocol.IMessageHandler;
import org.osami.gateway.sensor.protocol.IMessageSender;
import org.osami.gateway.sensor.protocol.IMessageSource;
import org.osami.gateway.sensor.protocol.IPayload;
import org.osami.gateway.sensor.protocol.IPayloadParser;
import org.osami.gateway.sensor.protocol.ISensorDevice;
import org.osami.gateway.sensor.protocol.Message;
import org.osami.gateway.sensor.protocol.PayloadParsingException;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
import org.osgi.service.device.Constants;

@Component
@Provides
public class Discovery implements IMessageHandler {
    private static final Logger logger = Logger.getLogger(Discovery.class.getName());

    private Map<ServiceReference, IMessageSource> sources;
    private Map<String, String> metadataMap;
    private Map<String, Dictionary<String, Object>> configurationMap;
    private List<ComponentInstance> componentInstances;

    @Requires(filter = "(factory.name=org.osami.gateway.sensor.internal.SensorDevice)")
    private Factory nodeFactory;

    @Requires
    private IPayloadParser parser;

    @Requires(filter = "(type=discovery)")
    private IMessageSender sender;

    private BundleContext bundleContext;

    public Discovery(BundleContext bundleContext) {
        this.bundleContext = bundleContext;
        this.componentInstances = new ArrayList<ComponentInstance>();
        this.sources = new Hashtable<ServiceReference, IMessageSource>();
        this.metadataMap = new TreeMap<String, String>();
        this.configurationMap = new TreeMap<String, Dictionary<String, Object>>();
    }

    @Validate
    public void start() {
        
    }

    @Invalidate
    public void stop() {
        synchronized (sources) {
            Set<ServiceReference> srs = sources.keySet();
            for (ServiceReference sr : srs) {
                IMessageSource source = sources.get(sr);
                if (source != null) {
                    source.unregisterHandler(this);
                }
            }
        }
    }

    @Bind(filter = "(type=discovery)", aggregate = true)
    public void bindMessageSource(IMessageSource source, ServiceReference reference) {
        logger.info("Discovery: bindMessageSource" + reference);
        source.registerHandler(this);
        synchronized (sources) {
            sources.put(reference, source);
        }
    }

    @Unbind(filter = "(type=discovery)")
    public void unbindMessageSource(ServiceReference reference) {
        logger.info("Discovery: unbindMessageSource" + reference);
        IMessageSource source = sources.get(reference);
        if (source != null) {
            source.unregisterHandler(this);
            synchronized (sources) {
                sources.remove(reference);
            }
        }
    }

    public synchronized void handle(Message message) {
        String payloadAsString = message.getPayload();
        // Need to parse payload
        IPayload payload = null;
        try {
            payload = parser.parse(payloadAsString);
            String nodeId = payload.getId();
            String expectedInstanceName = "org.osami.gateway.sensor.internal.SensorNode_" + nodeId;

            if (IPayload.Type.HELLO.equals(payload.getType())) {
                HelloPayload hello = (HelloPayload) payload;
                Message replyMessage;

                logger.info("From node " + nodeId + " got 1 hello: " + hello);

                try {
                    replyMessage = new MulticastOutboundMessage("", hello.getPort(), hello.getHost(), "", IPayload.Type.GET_METADATA);
                    sender.send(replyMessage);
                }
                catch (Exception e) {
                    e.printStackTrace();
                }
            }
            else if (IPayload.Type.BYE.equals(payload.getType())) {
                ByePayload bye = (ByePayload) payload;

                logger.info("From node " + nodeId + " got 1 bye: " + bye);

                for (ComponentInstance instance : componentInstances) {
                    String instanceName = instance.getInstanceName();
                    if (instanceName.equals(expectedInstanceName)) {
                        logger.info("Found existing SensorNode instance for " + nodeId + ", disposing it");
                        configurationMap.remove(nodeId);
                        metadataMap.remove(nodeId);
                        componentInstances.remove(instance);
                        instance.dispose();
                        return;
                    }
                }
                logger.severe("Got 1 bye but found no matching SensorNode instance !");
            }
            else if (IPayload.Type.METADATA.equals(payload.getType())) {
                MetadataPayload metadata = (MetadataPayload) payload;
                Map<String, String> values = metadata.getValues();

                logger.info("From node " + nodeId + " got 1 metadata: " + metadata);

                for (ComponentInstance instance : componentInstances) {
                    String instanceName = instance.getInstanceName();
                    if (instanceName.equals(expectedInstanceName)) {
                        String oldMetadata = metadataMap.get(nodeId);
                        if (oldMetadata != null) {
                            if (oldMetadata.equals(metadata.toString())) {
                                logger.info("Found existing SensorNode instance for " + nodeId + " and metadata hasn't changed, not creating an instance");
                                return;
                            }
                            else {
                                logger.info("Found existing SensorNode instance for " + nodeId + " but metadata has changed, updating instance configuration !");
                                Dictionary<String, Object> instanceConfiguration = configurationMap.get(nodeId);
                                instanceConfiguration.put("device.metadata", values);
                                configurationMap.put(nodeId, instanceConfiguration);
                                metadataMap.put(nodeId, metadata.toString());
                                instance.reconfigure(instanceConfiguration);
                                try {
                                    ServiceReference[] refs = bundleContext.getServiceReferences(ISensorDevice.class.getName(), "(instance.name=" + instance.getInstanceName() +")");
                                    if (refs != null) {
                                        ISensorDevice sensorDevice = (ISensorDevice) (bundleContext.getService(refs[0]));
                                        sensorDevice.notifyConfigurationChange();
                                    }
                                }
                                catch (Exception e) {
                                    // Should not happen
                                    e.printStackTrace();
                                }

                                return;
                            }
                        }
                        else {
                            logger.severe("BUG: no metadata corresponding to existing instance !");
                            return;
                        }
                    }
                }
                logger.info("Creating SensorNode instance for " + nodeId + ".");
                try {
                    Dictionary<String, Object> bindFilters = new Hashtable<String, Object>();
                    bindFilters.put("senses", "(device.id=" + nodeId + ")");

                    Dictionary<String, Object> instanceConfiguration = new Hashtable<String, Object>();
                    instanceConfiguration.put(Constants.DEVICE_SERIAL, nodeId);
                    instanceConfiguration.put("instance.name", expectedInstanceName);
                    instanceConfiguration.put("requires.filters", bindFilters);
                    instanceConfiguration.put("device.metadata", values);

                    // Call factory for instance creation
                    ComponentInstance ci = nodeFactory.createComponentInstance(instanceConfiguration);
                    componentInstances.add(ci);

                    metadataMap.put(nodeId, metadata.toString());
                    configurationMap.put(nodeId, instanceConfiguration);

                    logger.info("SensorNode instance for " + nodeId + " created.");
                } catch (Throwable t) {
                    t.printStackTrace();
                }
            }
        } catch (PayloadParsingException e) {
            e.printStackTrace();
        }
    }
}
